{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 第四章 构建RAG应用\n",
    "\n",
    "## 4.1 将LLM 接入 LangChain"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "LangChain 为基于 LLM 开发自定义应用提供了高效的开发框架，便于开发者迅速地激发 LLM 的强大能力，搭建 LLM 应用。LangChain 也同样支持多种大模型，内置了 OpenAI、LLAMA 等大模型的调用接口。但是，LangChain 并没有内置所有大模型，它通过允许用户自定义 LLM 类型，来提供强大的可扩展性。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.1.1 基于 LangChain 调用 ChatGPT"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "LangChain 提供了对于多种大模型的封装，基于 LangChain 的接口可以便捷地调用 ChatGPT 并将其集合在以 LangChain 为基础框架搭建的个人应用中。我们在此简述如何使用 LangChain 接口来调用 ChatGPT。\n",
    "\n",
    "注意，基于 LangChain 接口调用 ChatGPT 同样需要配置你的个人密钥，配置方法同上。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "从 `langchain.chat_models` 导入 `OpenAI` 的对话模型 `ChatOpenAI` 。 除去OpenAI以外，`langchain.chat_models` 还集成了其他对话模型，更多细节可以查看[Langchain官方文档](https://api.python.langchain.com/en/latest/langchain_api_reference.html#module-langchain.chat_models)。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from dotenv import load_dotenv, find_dotenv\n",
    "\n",
    "# 读取本地/项目的环境变量。\n",
    "\n",
    "# find_dotenv()寻找并定位.env文件的路径\n",
    "# load_dotenv()读取该.env文件，并将其中的环境变量加载到当前的运行环境中  \n",
    "# 如果你设置的是全局的环境变量，这行代码则没有任何作用。\n",
    "_ = load_dotenv(find_dotenv())\n",
    "\n",
    "# 获取环境变量 OPENAI_API_KEY\n",
    "openai_api_key = os.environ['OPENAI_API_KEY']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "没有安装 langchain-openai 的话，请先运行下面进行代码！"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来你需要实例化一个 ChatOpenAI 类，可以在实例化时传入超参数来控制回答，例如 `temperature` 参数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ChatOpenAI(client=<openai.resources.chat.completions.completions.Completions object at 0x117efa8f0>, async_client=<openai.resources.chat.completions.completions.AsyncCompletions object at 0x117f14e20>, root_client=<openai.OpenAI object at 0x1157c7d30>, root_async_client=<openai.AsyncOpenAI object at 0x117efa950>, temperature=0.0, model_kwargs={}, openai_api_key=SecretStr('**********'))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 这里我们将参数temperature设置为0.0，从而减少生成答案的随机性。\n",
    "# 如果你想要每次得到不一样的有新意的答案，可以尝试调整该参数。\n",
    "llm = ChatOpenAI(temperature=0.0)\n",
    "llm"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上面的 cell 假设你的 OpenAI API 密钥是在环境变量中设置的，如果您希望手动指定API密钥，请使用以下代码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# llm = ChatOpenAI(temperature=0, openai_api_key=\"YOUR_API_KEY\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，默认调用的是 ChatGPT-3.5 模型。另外，几种常用的超参数设置包括：\n",
    "\n",
    "    · model_name：所要使用的模型，默认为 ‘gpt-3.5-turbo’，参数设置与 OpenAI 原生接口参数设置一致。\n",
    "\n",
    "    · temperature：温度系数，取值同原生接口。\n",
    "\n",
    "    · openai_api_key：OpenAI API key，如果不使用环境变量设置 API Key，也可以在实例化时设置。\n",
    "\n",
    "    · openai_proxy：设置代理，如果不使用环境变量设置代理，也可以在实例化时设置。\n",
    "\n",
    "    · streaming：是否使用流式传输，即逐字输出模型回答，默认为 False，此处不赘述。\n",
    "\n",
    "    · max_tokens：模型输出的最大 token 数，意义及取值同上。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "当我们初始化了你选择的`LLM`后，我们就可以尝试使用它！让我们问一下“请你自我介绍一下自己！”"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "output = llm.invoke(\"请你自我介绍一下自己！\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='你好，我是一个智能助手，专门为您提供各种服务和帮助。我可以回答您的问题，提供信息和建议，帮助您解决问题。如果您有任何需要，请随时告诉我，我会尽力帮助您。感谢您选择我作为您的助手！如果您有任何问题，请随时问我。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 109, 'prompt_tokens': 20, 'total_tokens': 129, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-c611b32a-4adf-47af-9b97-6dda68a117e1-0', usage_metadata={'input_tokens': 20, 'output_tokens': 109, 'total_tokens': 129})"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "output"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在我们开发大模型应用时，大多数情况下不会直接将用户的输入直接传递给 LLM。通常，他们会将用户输入添加到一个较大的文本中，称为`提示模板`，该文本提供有关当前特定任务的附加上下文。\n",
    "PromptTemplates 正是帮助解决这个问题！它们捆绑了从用户输入到完全格式化的提示的所有逻辑。这可以非常简单地开始 - 例如，生成上述字符串的提示就是：\n",
    "\n",
    "\n",
    "我们需要先构造一个个性化 Template："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 这里我们要求模型对给定文本进行中文翻译\n",
    "prompt = \"\"\"请你将由三个反引号分割的文本翻译成英文！\\\n",
    "text: ```{text}```\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来让我们看一下构造好的完整的提示模版："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'请你将由三个反引号分割的文本翻译成英文！text: ```我带着比身体重的行李，游入尼罗河底，经过几道闪电 看到一堆光圈，不确定是不是这里。```\\n'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "text = \"我带着比身体重的行李，\\\n",
    "游入尼罗河底，\\\n",
    "经过几道闪电 看到一堆光圈，\\\n",
    "不确定是不是这里。\\\n",
    "\"\n",
    "prompt.format(text=text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们知道聊天模型的接口是基于消息（message），而不是原始的文本。PromptTemplates 也可以用于产生消息列表，在这种样例中，`prompt`不仅包含了输入内容信息，也包含了每条`message`的信息(角色、在列表中的位置等)。通常情况下，一个 `ChatPromptTemplate` 是一个 `ChatMessageTemplate` 的列表。每个 `ChatMessageTemplate` 包含格式化该聊天消息的说明（其角色以及内容）。\n",
    "\n",
    "让我们一起看一个示例："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[SystemMessage(content='你是一个翻译助手，可以帮助我将 中文 翻译成 英文.', additional_kwargs={}, response_metadata={}),\n",
       " HumanMessage(content='我带着比身体重的行李，游入尼罗河底，经过几道闪电 看到一堆光圈，不确定是不是这里。', additional_kwargs={}, response_metadata={})]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "\n",
    "template = \"你是一个翻译助手，可以帮助我将 {input_language} 翻译成 {output_language}.\"\n",
    "human_template = \"{text}\"\n",
    "\n",
    "chat_prompt = ChatPromptTemplate([\n",
    "    (\"system\", template),\n",
    "    (\"human\", human_template),\n",
    "])\n",
    "\n",
    "text = \"我带着比身体重的行李，\\\n",
    "游入尼罗河底，\\\n",
    "经过几道闪电 看到一堆光圈，\\\n",
    "不确定是不是这里。\\\n",
    "\"\n",
    "messages  = chat_prompt.invoke({\"input_language\": \"中文\", \"output_language\": \"英文\", \"text\": text})\n",
    "messages"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来让我们调用定义好的`llm`和`messages`来输出回答："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='I carried luggage heavier than my body and dived into the bottom of the Nile River. After passing through several flashes of lightning, I saw a pile of halos, not sure if this is the place.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 43, 'prompt_tokens': 95, 'total_tokens': 138, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-c58ae91f-7c0a-4f20-ad7f-8d6421f3a9aa-0', usage_metadata={'input_tokens': 95, 'output_tokens': 43, 'total_tokens': 138})"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "output  = llm.invoke(messages)\n",
    "output"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OutputParsers 将语言模型的原始输出转换为可以在下游使用的格式。 OutputParsers 有几种主要类型，包括：\n",
    "- 将 LLM 文本转换为结构化信息（例如 JSON） \n",
    "- 将 ChatMessage 转换为字符串 \n",
    "- 将除消息之外的调用返回的额外信息（如 OpenAI 函数调用）转换为字符串\n",
    "\n",
    "最后，我们将模型输出传递给 `output_parser`，它是一个 `BaseOutputParser`，这意味着它接受**字符串或 BaseMessage 作为输入**。 StrOutputParser 特别简单地将任何输入转换为字符串。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'I carried luggage heavier than my body and dived into the bottom of the Nile River. After passing through several flashes of lightning, I saw a pile of halos, not sure if this is the place.'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langchain_core.output_parsers import StrOutputParser\n",
    "\n",
    "output_parser = StrOutputParser()\n",
    "output_parser.invoke(output)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "从上面结果可以看到，我们通过输出解析器成功将 `ChatMessage` 类型的输出解析为了`字符串`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们现在可以将所有这些组合成一条链。该链将获取输入变量，将这些变量传递给提示模板以创建提示，将提示传递给语言模型，然后通过（可选）输出解析器传递输出。接下来我们将使用LCEL这种语法去快速实现一条链（chain）。让我们看看它的实际效果！"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'I carried luggage heavier than my body weight and dived into the bottom of the Nile River. After passing through several flashes of lightning, I saw a pile of halos, not sure if this is the place.'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "chain = chat_prompt | llm | output_parser\n",
    "chain.invoke({\"input_language\":\"中文\", \"output_language\":\"英文\",\"text\": text})\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "再测试一个样例："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'我扛着比我的身体还重的行李，潜入尼罗河的底部。穿过几道闪电后，我看到一堆光环，不确定这是否就是目的地。'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "text = 'I carried luggage heavier than my body and dived into the bottom of the Nile River. After passing through several flashes of lightning, I saw a pile of halos, not sure if this is the place.'\n",
    "chain.invoke({\"input_language\": \"英文\", \"output_language\": \"中文\",\"text\": text})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> 什么是 LCEL ？ \n",
    "LCEL（LangChain Expression Language，Langchain的表达式语言），LCEL是一种新的语法，是LangChain工具包的重要补充，他有许多优点，使得我们处理LangChain和代理更加简单方便。\n",
    "\n",
    "- LCEL提供了异步、批处理和流处理支持，使代码可以快速在不同服务器中移植。\n",
    "- LCEL拥有后备措施，解决LLM格式输出的问题。\n",
    "- LCEL增加了LLM的并行性，提高了效率。\n",
    "- LCEL内置了日志记录，即使代理变得复杂，有助于理解复杂链条和代理的运行情况。\n",
    "\n",
    "用法示例：\n",
    "\n",
    "`chain = prompt | model | output_parser`\n",
    "\n",
    "上面代码中我们使用 LCEL 将不同的组件拼凑成一个链，在此链中，用户输入传递到提示模板，然后提示模板输出传递到模型，然后模型输出传递到输出解析器。| 的符号类似于 Unix 管道运算符，它将不同的组件链接在一起，将一个组件的输出作为下一个组件的输入。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.1.2 使用 LangChain 调用百度文心一言"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们同样可以通过 LangChain 框架来调用百度文心大模型，以将文心模型接入到我们的应用框架中。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "from dotenv import find_dotenv, load_dotenv\n",
    "import os\n",
    "\n",
    "# 读取本地/项目的环境变量。\n",
    "\n",
    "# find_dotenv()寻找并定位.env文件的路径\n",
    "# load_dotenv()读取该.env文件，并将其中的环境变量加载到当前的运行环境中\n",
    "# 如果你设置的是全局的环境变量，这行代码则没有任何作用。\n",
    "_ = load_dotenv(find_dotenv())\n",
    "\n",
    "# 获取环境变量 API_KEY\n",
    "QIANFAN_AK = os.environ[\"QIANFAN_AK\"]\n",
    "QIANFAN_SK = os.environ[\"QIANFAN_SK\"]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[WARNING][2025-03-05 19:41:13.652] redis_rate_limiter.py:21 [t:8258539328]: No redis installed, RedisRateLimiter unavailable. Ignore this warning if you don't need to use qianfan SDK in distribution environment\n",
      "/var/folders/yd/c4q_f88j1l70g7_jcb6pdnb80000gn/T/ipykernel_51550/1209153611.py:4: LangChainDeprecationWarning: The method `BaseLLM.__call__` was deprecated in langchain-core 0.1.7 and will be removed in 1.0. Use :meth:`~invoke` instead.\n",
      "  res = llm(\"你好，请你自我介绍一下！\")\n",
      "[ERROR][2025-03-05 19:41:13.835] base.py:134 [t:8258539328]: http request url https://qianfan.baidubce.com/wenxinworkshop/service/list failed with http status code 403\n",
      "error code from baidu: IamSignatureInvalid\n",
      "error message from baidu: IamSignatureInvalid, cause: Could not find credential.\n",
      "request headers: {'User-Agent': 'python-requests/2.32.3', 'Accept-Encoding': 'gzip, deflate, zstd', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'application/json', 'Host': 'qianfan.baidubce.com', 'request-source': 'qianfan_py_sdk_v0.4.12.3', 'x-bce-date': '2025-03-05T11:41:13Z', 'Authorization': 'bce-auth-v1//2025-03-05T11:41:13Z/300/x-bce-date;host;request-source;content-type/cc383f75803c577d6486841dc228aea994102a4b70bd5ff76f27d12bdb7af133', 'Content-Length': '2'}\n",
      "request body: '{}'\n",
      "response headers: {'Content-Length': '0', 'Date': 'Wed, 05 Mar 2025 11:41:13 GMT', 'X-Bce-Error-Code': 'IamSignatureInvalid', 'X-Bce-Error-Message': 'IamSignatureInvalid, cause: Could not find credential.', 'X-Bce-Exception-Point': 'Gateway', 'X-Bce-Gateway-Region': 'BJ', 'X-Bce-Request-Id': '17059251-201a-4a1f-8fbc-220df83ff184', 'Content-Type': 'text/plain; charset=utf-8'}\n",
      "response body: b''\n",
      "[WARNING][2025-03-05 19:41:13.835] base.py:1083 [t:8258539328]: fetch_supported_models failed: http request url https://qianfan.baidubce.com/wenxinworkshop/service/list failed with http status code 403\n",
      "error code from baidu: IamSignatureInvalid\n",
      "error message from baidu: IamSignatureInvalid, cause: Could not find credential.\n",
      "request headers: {'User-Agent': 'python-requests/2.32.3', 'Accept-Encoding': 'gzip, deflate, zstd', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'application/json', 'Host': 'qianfan.baidubce.com', 'request-source': 'qianfan_py_sdk_v0.4.12.3', 'x-bce-date': '2025-03-05T11:41:13Z', 'Authorization': 'bce-auth-v1//2025-03-05T11:41:13Z/300/x-bce-date;host;request-source;content-type/cc383f75803c577d6486841dc228aea994102a4b70bd5ff76f27d12bdb7af133', 'Content-Length': '2'}\n",
      "request body: '{}'\n",
      "response headers: {'Content-Length': '0', 'Date': 'Wed, 05 Mar 2025 11:41:13 GMT', 'X-Bce-Error-Code': 'IamSignatureInvalid', 'X-Bce-Error-Message': 'IamSignatureInvalid, cause: Could not find credential.', 'X-Bce-Exception-Point': 'Gateway', 'X-Bce-Gateway-Region': 'BJ', 'X-Bce-Request-Id': '17059251-201a-4a1f-8fbc-220df83ff184', 'Content-Type': 'text/plain; charset=utf-8'}\n",
      "response body: b''\n",
      "[ERROR][2025-03-05 19:41:14.033] base.py:134 [t:8258539328]: http request url https://qianfan.baidubce.com/wenxinworkshop/service/list failed with http status code 403\n",
      "error code from baidu: IamSignatureInvalid\n",
      "error message from baidu: IamSignatureInvalid, cause: Could not find credential.\n",
      "request headers: {'User-Agent': 'python-requests/2.32.3', 'Accept-Encoding': 'gzip, deflate, zstd', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'application/json', 'Host': 'qianfan.baidubce.com', 'request-source': 'qianfan_py_sdk_v0.4.12.3', 'x-bce-date': '2025-03-05T11:41:13Z', 'Authorization': 'bce-auth-v1//2025-03-05T11:41:13Z/300/x-bce-date;host;request-source;content-type/cc383f75803c577d6486841dc228aea994102a4b70bd5ff76f27d12bdb7af133', 'Content-Length': '2'}\n",
      "request body: '{}'\n",
      "response headers: {'Content-Length': '0', 'Date': 'Wed, 05 Mar 2025 11:41:14 GMT', 'X-Bce-Error-Code': 'IamSignatureInvalid', 'X-Bce-Error-Message': 'IamSignatureInvalid, cause: Could not find credential.', 'X-Bce-Exception-Point': 'Gateway', 'X-Bce-Gateway-Region': 'BJ', 'X-Bce-Request-Id': '09088182-5e6e-4725-bd6c-e1f476287b34', 'Content-Type': 'text/plain; charset=utf-8'}\n",
      "response body: b''\n",
      "[WARNING][2025-03-05 19:41:14.034] base.py:1083 [t:8258539328]: fetch_supported_models failed: http request url https://qianfan.baidubce.com/wenxinworkshop/service/list failed with http status code 403\n",
      "error code from baidu: IamSignatureInvalid\n",
      "error message from baidu: IamSignatureInvalid, cause: Could not find credential.\n",
      "request headers: {'User-Agent': 'python-requests/2.32.3', 'Accept-Encoding': 'gzip, deflate, zstd', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'application/json', 'Host': 'qianfan.baidubce.com', 'request-source': 'qianfan_py_sdk_v0.4.12.3', 'x-bce-date': '2025-03-05T11:41:13Z', 'Authorization': 'bce-auth-v1//2025-03-05T11:41:13Z/300/x-bce-date;host;request-source;content-type/cc383f75803c577d6486841dc228aea994102a4b70bd5ff76f27d12bdb7af133', 'Content-Length': '2'}\n",
      "request body: '{}'\n",
      "response headers: {'Content-Length': '0', 'Date': 'Wed, 05 Mar 2025 11:41:14 GMT', 'X-Bce-Error-Code': 'IamSignatureInvalid', 'X-Bce-Error-Message': 'IamSignatureInvalid, cause: Could not find credential.', 'X-Bce-Exception-Point': 'Gateway', 'X-Bce-Gateway-Region': 'BJ', 'X-Bce-Request-Id': '09088182-5e6e-4725-bd6c-e1f476287b34', 'Content-Type': 'text/plain; charset=utf-8'}\n",
      "response body: b''\n",
      "[ERROR][2025-03-05 19:41:14.216] base.py:134 [t:8258539328]: http request url https://qianfan.baidubce.com/wenxinworkshop/service/list failed with http status code 403\n",
      "error code from baidu: IamSignatureInvalid\n",
      "error message from baidu: IamSignatureInvalid, cause: Could not find credential.\n",
      "request headers: {'User-Agent': 'python-requests/2.32.3', 'Accept-Encoding': 'gzip, deflate, zstd', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'application/json', 'Host': 'qianfan.baidubce.com', 'request-source': 'qianfan_py_sdk_v0.4.12.3', 'x-bce-date': '2025-03-05T11:41:14Z', 'Authorization': 'bce-auth-v1//2025-03-05T11:41:14Z/300/x-bce-date;host;request-source;content-type/34d382a0332f9213819d512ca7cd9bf264d3126e0454764341daa2ed7c9bf1bb', 'Content-Length': '2'}\n",
      "request body: '{}'\n",
      "response headers: {'Content-Length': '0', 'Date': 'Wed, 05 Mar 2025 11:41:14 GMT', 'X-Bce-Error-Code': 'IamSignatureInvalid', 'X-Bce-Error-Message': 'IamSignatureInvalid, cause: Could not find credential.', 'X-Bce-Exception-Point': 'Gateway', 'X-Bce-Gateway-Region': 'BJ', 'X-Bce-Request-Id': '8d60a700-e5c1-42af-93cc-02e817421476', 'Content-Type': 'text/plain; charset=utf-8'}\n",
      "response body: b''\n",
      "[WARNING][2025-03-05 19:41:14.217] base.py:1083 [t:8258539328]: fetch_supported_models failed: http request url https://qianfan.baidubce.com/wenxinworkshop/service/list failed with http status code 403\n",
      "error code from baidu: IamSignatureInvalid\n",
      "error message from baidu: IamSignatureInvalid, cause: Could not find credential.\n",
      "request headers: {'User-Agent': 'python-requests/2.32.3', 'Accept-Encoding': 'gzip, deflate, zstd', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'application/json', 'Host': 'qianfan.baidubce.com', 'request-source': 'qianfan_py_sdk_v0.4.12.3', 'x-bce-date': '2025-03-05T11:41:14Z', 'Authorization': 'bce-auth-v1//2025-03-05T11:41:14Z/300/x-bce-date;host;request-source;content-type/34d382a0332f9213819d512ca7cd9bf264d3126e0454764341daa2ed7c9bf1bb', 'Content-Length': '2'}\n",
      "request body: '{}'\n",
      "response headers: {'Content-Length': '0', 'Date': 'Wed, 05 Mar 2025 11:41:14 GMT', 'X-Bce-Error-Code': 'IamSignatureInvalid', 'X-Bce-Error-Message': 'IamSignatureInvalid, cause: Could not find credential.', 'X-Bce-Exception-Point': 'Gateway', 'X-Bce-Gateway-Region': 'BJ', 'X-Bce-Request-Id': '8d60a700-e5c1-42af-93cc-02e817421476', 'Content-Type': 'text/plain; charset=utf-8'}\n",
      "response body: b''\n",
      "[INFO][2025-03-05 19:41:14.219] oauth.py:277 [t:8258539328]: trying to refresh token for ak `6hM0ZG***`\n",
      "[INFO][2025-03-05 19:41:14.340] oauth.py:304 [t:8258539328]: successfully refresh token\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "你好！我是一个人工智能语言模型，我的名字是文心一言。我能够与人进行自然语言交互，并提供各种信息和服务。如果你有任何问题或需要帮助，请随时告诉我，我会尽力为你提供帮助。\n"
     ]
    }
   ],
   "source": [
    "from langchain_community.llms.baidu_qianfan_endpoint import QianfanLLMEndpoint\n",
    "\n",
    "llm = QianfanLLMEndpoint(streaming=True)\n",
    "res = llm(\"你好，请你自我介绍一下！\")\n",
    "print(res)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.1.3 讯飞星火"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们同样可以通过 LangChain 框架来调用讯飞星火大模型，更多信息参考[SparkLLM](https://python.langchain.com/docs/integrations/llms/sparkllm)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们希望像调用 ChatGPT 那样直接将秘钥存储在 .env 文件中，并将其加载到环境变量，从而隐藏秘钥的具体细节，保证安全性。因此，我们需要在 .env 文件中配置`IFLYTEK_SPARK_APP_ID`、 `IFLYTEK_SPARK_API_KEY` 和 `IFLYTEK_SPARK_API_SECRET`，并使用以下代码加载："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dotenv import find_dotenv, load_dotenv\n",
    "import os\n",
    "\n",
    "# 读取本地/项目的环境变量。\n",
    "\n",
    "# find_dotenv()寻找并定位.env文件的路径\n",
    "# load_dotenv()读取该.env文件，并将其中的环境变量加载到当前的运行环境中\n",
    "# 如果你设置的是全局的环境变量，这行代码则没有任何作用。\n",
    "_ = load_dotenv(find_dotenv())\n",
    "\n",
    "# 获取环境变量 API_KEY\n",
    "IFLYTEK_SPARK_APP_ID = os.environ[\"IFLYTEK_SPARK_APP_ID\"]\n",
    "IFLYTEK_SPARK_API_KEY = os.environ[\"IFLYTEK_SPARK_API_KEY\"]\n",
    "IFLYTEK_SPARK_API_SECRET = os.environ[\"IFLYTEK_SPARK_API_SECRET\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "另外星火各模型对应的`spark_api_url`与`spark_llm_domain`均不相同，可以参考[接口说明](https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E)选择调用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.llms.sparkllm import SparkLLM\n",
    "\n",
    "# Load the model\n",
    "llm = SparkLLM(\n",
    "    model='Spark4.0 Ultra',\n",
    "    app_id=IFLYTEK_SPARK_APP_ID,\n",
    "    api_key=IFLYTEK_SPARK_API_KEY,\n",
    "    api_secret=IFLYTEK_SPARK_API_SECRET,\n",
    "    spark_api_url=\"wss://spark-api.xf-yun.com/v4.0/chat\",\n",
    "    spark_llm_domain=\"4.0Ultra\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "您好，我是科大讯飞研发的认知智能大模型，我的名字叫讯飞星火认知大模型。我可以和人类进行自然交流，解答问题，高效完成各领域认知智能需求。\n"
     ]
    }
   ],
   "source": [
    "res = llm.invoke(\"你好，请你自我介绍一下！\")\n",
    "print(res)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "从而我们可以将星火大模型加入到 LangChain 架构中，实现在应用中对文心大模型的调用。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.1.4 使用 LangChain 调用智谱 GLM"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们同样可以通过 LangChain 框架来调用智谱 AI 大模型，以将其接入到我们的应用框架中。由于 langchain 中提供的[ChatGLM](https://python.langchain.com/docs/integrations/llms/chatglm)已不可用，因此我们需要自定义一个LLM。\n",
    "\n",
    "如果你使用的是智谱 GLM API，你需要将我们封装的代码[zhipuai_llm.py](./zhipuai_llm.py)下载到本 Notebook 的同级目录下，才可以运行下列代码来在 LangChain 中使用 GLM。\n",
    "\n",
    "根据智谱官方宣布以下模型即将弃用，在这些模型弃用后，会将它们自动路由至新的模型。请用户注意在弃用日期之前，将您的模型编码更新为最新版本，以确保服务的顺畅过渡，更多模型相关信息请访问[model](https://open.bigmodel.cn/dev/howuse/model)\n",
    "\n",
    "| 模型编码 |弃用日期|指向模型|\n",
    "| ---- | ---- | ---- |\n",
    "|chatglm_pro|2024 年 12 月 31 日|glm-4|\n",
    "|chatglm_std|2024 年 12 月 31 日|glm-3-turbo|\n",
    "|chatglm_lite|2024 年 12 月 31 日|glm-3-turbo|"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from zhipuai_llm import ZhipuaiLLM\n",
    "from dotenv import find_dotenv, load_dotenv\n",
    "import os\n",
    "\n",
    "# 读取本地/项目的环境变量。\n",
    "\n",
    "# find_dotenv()寻找并定位.env文件的路径\n",
    "# load_dotenv()读取该.env文件，并将其中的环境变量加载到当前的运行环境中\n",
    "# 如果你设置的是全局的环境变量，这行代码则没有任何作用。\n",
    "_ = load_dotenv(find_dotenv())\n",
    "\n",
    "# 获取环境变量 API_KEY\n",
    "api_key = os.environ[\"ZHIPUAI_API_KEY\"] #填写控制台中获取的 APIKey 信息"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "zhipuai_model = ZhipuaiLLM(model_name=\"glm-4-plus\", temperature=0.1, api_key=api_key)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='你好！我是人工智能助手智谱清言（ChatGLM），是基于智谱 AI 公司于 2024 年训练的语言模型开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。', additional_kwargs={}, response_metadata={'time_in_seconds': 1.87}, id='run-4e509a7e-9859-4acb-9418-23245fa5b7a7-0', usage_metadata={'input_tokens': 11, 'output_tokens': 42, 'total_tokens': 53})"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "zhipuai_model.invoke(\"你好，请你自我介绍一下！\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4.2 构建检索问答链"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在 `C3 搭建数据库` 章节，我们已经介绍了如何根据自己的本地知识文档，搭建一个向量知识库。 在接下来的内容里，我们将使用搭建好的向量数据库，对 query 查询问题进行召回，并将召回结果和 query 结合起来构建 prompt，输入到大模型中进行问答。   "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.2.1 加载向量数据库\n",
    "\n",
    "首先，我们加载在前一章已经构建的向量数据库。注意，此处你需要使用和构建时相同的 Emedding。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "sys.path.append(\"../C3 搭建知识库\") # 将父目录放入系统路径中\n",
    "\n",
    "# 使用智谱 Embedding API，注意，需要将上一章实现的封装代码下载到本地\n",
    "from zhipuai_embedding import ZhipuAIEmbeddings\n",
    "\n",
    "from langchain.vectorstores.chroma import Chroma"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "从环境变量中加载你的 API_KEY"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dotenv import load_dotenv, find_dotenv\n",
    "import os\n",
    "\n",
    "_ = load_dotenv(find_dotenv())    # read local .env file\n",
    "zhipuai_api_key = os.environ['ZHIPUAI_API_KEY']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "加载向量数据库，其中包含了 ../../data_base/knowledge_db 下多个文档的 Embedding"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/var/folders/yd/c4q_f88j1l70g7_jcb6pdnb80000gn/T/ipykernel_17468/4214663976.py:8: LangChainDeprecationWarning: The class `Chroma` was deprecated in LangChain 0.2.9 and will be removed in 1.0. An updated version of the class exists in the :class:`~langchain-chroma package and should be used instead. To use it run `pip install -U :class:`~langchain-chroma` and import as `from :class:`~langchain_chroma import Chroma``.\n",
      "  vectordb = Chroma(\n"
     ]
    }
   ],
   "source": [
    "# 定义 Embeddings\n",
    "embedding = ZhipuAIEmbeddings()\n",
    "\n",
    "# 向量数据库持久化路径\n",
    "persist_directory = '../../data_base/vector_db/chroma'\n",
    "\n",
    "# 加载数据库\n",
    "vectordb = Chroma(\n",
    "    persist_directory=persist_directory,  # 允许我们将persist_directory目录保存到磁盘上\n",
    "    embedding_function=embedding\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "向量库中存储的数量：1004\n"
     ]
    }
   ],
   "source": [
    "print(f\"向量库中存储的数量：{vectordb._collection.count()}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以测试一下加载的向量数据库，我们可以通过`as_retriever`方法把向量数据库构造成检索器。我们使用一个问题 query 进行向量检索。如下代码会在向量数据库中根据相似性进行检索，返回前 k 个最相似的文档。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "检索到的内容数：3\n"
     ]
    }
   ],
   "source": [
    "question = \"什么是prompt engineering?\"\n",
    "retriever = vectordb.as_retriever(search_kwargs={\"k\": 3})\n",
    "docs = retriever.invoke(question)\n",
    "print(f\"检索到的内容数：{len(docs)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "打印一下检索到的内容"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "检索到的第0个内容: \n",
      " 具体来说，首先编写初版 Prompt，然后通过多轮调整逐步改进，直到生成了满意的结果。对于更复杂的应用，可以在多个样本上进行迭代训练，评估 Prompt 的平均表现。在应用较为成熟后，才需要采用在多个样本集上评估 Prompt 性能的方式来进行细致优化。因为这需要较高的计算资源。\n",
      "\n",
      "总之，Prompt 工程师的核心是掌握 Prompt 的迭代开发和优化技巧，而非一开始就要求100%完美。通过不断调整试错，最终找到可靠适用的 Prompt 形式才是设计 Prompt 的正确方法。\n",
      "\n",
      "读者可以在 Jupyter Notebook 上，对本章给出的示例进行实践，修改 Prompt 并观察不同输出，以深入理解 Prompt 迭代优化的过程。这会对进一步开发复杂语言模型应用提供很好的实践准备。\n",
      "\n",
      "三、英文版\n",
      "\n",
      "产品说明书\n",
      "-----------------------------------------------------\n",
      "检索到的第1个内容: \n",
      " 第一章 简介\n",
      "\n",
      "欢迎来到面向开发者的提示工程部分，本部分内容基于吴恩达老师的《Prompt Engineering for Developer》课程进行编写。《Prompt Engineering for Developer》课程是由吴恩达老师与 OpenAI 技术团队成员 Isa Fulford 老师合作授课，Isa 老师曾开发过受欢迎的 ChatGPT 检索插件，并且在教授 LLM （Large Language Model， 大语言模型）技术在产品中的应用方面做出了很大贡献。她还参与编写了教授人们使用 Prompt 的 OpenAI cookbook。我们希望通过本模块的学习，与大家分享使用提示词开发 LLM 应用的最佳实践和技巧。\n",
      "-----------------------------------------------------\n",
      "检索到的第2个内容: \n",
      " 第二章 提示原则\n",
      "\n",
      "如何去使用 Prompt，以充分发挥 LLM 的性能？首先我们需要知道设计 Prompt 的原则，它们是每一个开发者设计 Prompt 所必须知道的基础概念。本章讨论了设计高效 Prompt 的两个关键原则：编写清晰、具体的指令和给予模型充足思考时间。掌握这两点，对创建可靠的语言模型交互尤为重要。\n",
      "\n",
      "首先，Prompt 需要清晰明确地表达需求，提供充足上下文，使语言模型准确理解我们的意图，就像向一个外星人详细解释人类世界一样。过于简略的 Prompt 往往使模型难以把握所要完成的具体任务。\n",
      "\n",
      "其次，让语言模型有充足时间推理也极为关键。就像人类解题一样，匆忙得出的结论多有失误。因此 Prompt 应加入逐步推理的要求，给模型留出充分思考时间，这样生成的结果才更准确可靠。\n",
      "\n",
      "如果 Prompt 在这两点上都作了优化，语言模型就能够尽可能发挥潜力，完成复杂的推理和生成任务。掌握这些 Prompt 设计原则，是开发者取得语言模型应用成功的重要一步。\n",
      "\n",
      "一、原则一 编写清晰、具体的指令\n",
      "-----------------------------------------------------\n"
     ]
    }
   ],
   "source": [
    "for i, doc in enumerate(docs):\n",
    "    print(f\"检索到的第{i}个内容: \\n {doc.page_content}\", end=\"\\n-----------------------------------------------------\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.2.2创建检索链\n",
    "我们可以使用LangChain的LCEL(LangChain Expression Language, LangChain表达式语言)来构建workflow，LCEL可以支持异步（ainvoke）、流式(stream)、批次处理(batch)等多种运行方式，同时还可以使用LangSmith无缝跟踪。\n",
    "\n",
    "接下来我们使用刚才定义的retriever定义一个简单的检索链。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'前言\\n“周志华老师的《机器学习》（西瓜书）是机器学习领域的经典入门教材之一，周老师为了使尽可能多的读\\n者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述，但是这对那些想深究公式推\\n导细节的读者来说可能“不太友好”，本书旨在对西瓜书里比较难理解的公式加以解析，以及对部分公式补充\\n具体的推导细节。”\\n读到这里，大家可能会疑问为啥前面这段话加了引号，因为这只是我们最初的遐想，后来我们了解到，周\\n老师之所以省去这些推导细节的真实原因是，他本尊认为“理工科数学基础扎实点的大二下学生应该对西瓜书\\n中的推导细节无困难吧，要点在书里都有了，略去的细节应能脑补或做练习”。所以...... 本南瓜书只能算是我\\n等数学渣渣在自学的时候记下来的笔记，希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二\\n下学生”。\\n使用说明\\n• 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的，所以南瓜书的最佳使用方法是以西瓜书\\n为主线，遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书；\\n• 对于初学机器学习的小白，西瓜书第1 章和第2 章的公式强烈不建议深究，简单过一下即可，等你学得\\n\\n最新版PDF 获取地址：https://github.com/datawhalechina/pumpkin-book/releases\\n编委会\\n主编：Sm1les、archwalker、jbb0523\\n编委：juxiao、Majingmin、MrBigFan、shanry、Ye980226\\n封面设计：构思-Sm1les、创作-林王茂盛\\n致谢\\n特别感谢awyd234、feijuan、Ggmatch、Heitao5200、huaqing89、LongJH、LilRachel、LeoLRH、Nono17、\\nspareribs、sunchaothu、StevenLzq 在最早期的时候对南瓜书所做的贡献。\\n扫描下方二维码，然后回复关键词“南瓜书”，即可加入“南瓜书读者交流群”\\n版权声明\\n本作品采用知识共享署名-非商业性使用-相同方式共享4.0 国际许可协议进行许可。\\n\\n• 对于初学机器学习的小白，西瓜书第1 章和第2 章的公式强烈不建议深究，简单过一下即可，等你学得\\n有点飘的时候再回来啃都来得及；\\n• 每个公式的解析和推导我们都力(zhi) 争(neng) 以本科数学基础的视角进行讲解，所以超纲的数学知识\\n我们通常都会以附录和参考文献的形式给出，感兴趣的同学可以继续沿着我们给的资料进行深入学习；\\n• 若南瓜书里没有你想要查阅的公式，或者你发现南瓜书哪个地方有错误，请毫不犹豫地去我们GitHub 的\\nIssues（地址：https://github.com/datawhalechina/pumpkin-book/issues）进行反馈，在对应版块\\n提交你希望补充的公式编号或者勘误信息，我们通常会在24 小时以内给您回复，超过24 小时未回复的\\n话可以微信联系我们（微信号：at-Sm1les）；\\n配套视频教程：https://www.bilibili.com/video/BV1Mh411e7VU\\n在线阅读地址：https://datawhalechina.github.io/pumpkin-book（仅供第1 版）'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langchain_core.runnables import RunnableLambda\n",
    "def combine_docs(docs):\n",
    "    return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
    "\n",
    "combiner = RunnableLambda(combine_docs)\n",
    "retrieval_chain = retriever | combiner\n",
    "\n",
    "retrieval_chain.invoke(\"南瓜书是什么？\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "LCEL中要求所有的组成元素都是`Runnable`类型，前面我们见过的`ChatModel`、`PromptTemplate`等都是继承自`Runnable`类。上方的`retrieval_chain`是由检索器`retriever`及组合器`combiner`组成的，由`|`符号串连，数据从左向右传递，即问题先被`retriever`检索得到检索结果，再被`combiner`进一步处理并输出。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.2.3 创建LLM"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在这里，我们调用 OpenAI 的 API 创建一个 LLM，当然你也可以使用其他 LLM 的 API 进行创建"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os \n",
    "OPENAI_API_KEY = os.environ[\"OPENAI_API_KEY\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'当然可以！我是一个由OpenAI开发的人工智能助手，名叫ChatGPT。我擅长处理和生成自然语言文本，可以帮助回答问题、提供信息、协助解决问题以及进行各种对话。我没有个人经历或情感，但我会尽力提供准确和有用的回答。如果你有任何问题或需要帮助的地方，随时可以问我！'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "llm = ChatOpenAI(model_name=\"gpt-4o\", temperature=0)\n",
    "\n",
    "llm.invoke(\"请你自我介绍一下自己！\").content"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.2.4 构建检索问答链"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import PromptTemplate\n",
    "from langchain_core.runnables import RunnablePassthrough, RunnableParallel\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "\n",
    "template = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。最多使用三句话。尽量使答案简明扼要。请你在回答的最后说“谢谢你的提问！”。\n",
    "{context}\n",
    "问题: {input}\n",
    "\"\"\"\n",
    "# 将template通过 PromptTemplate 转为可以在LCEL中使用的类型\n",
    "prompt = PromptTemplate(template=template)\n",
    "\n",
    "qa_chain = (\n",
    "    RunnableParallel({\"context\": retrieval_chain, \"input\": RunnablePassthrough()})\n",
    "    | prompt\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在上边代码中我们把刚才定义的检索链当作子链作为`prompt`的`context`，再使用`RunnablePassthrough`存储用户的问题作为`prompt`的`input`。又因为这两个操作是并行的，所以我们使用`RunnableParallel`来将他们并行运行。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**检索问答链效果测试**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "question_1 = \"什么是南瓜书？\"\n",
    "question_2 = \"Prompt Engineering for Developer是谁写的？\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "大模型+知识库后回答 question_1 的结果：\n",
      "南瓜书是一本对《机器学习》（西瓜书）中较难理解的公式进行解析和补充推导细节的书籍，旨在帮助读者更好地理解机器学习中的数学推导。它以西瓜书的内容为前置知识，适合在遇到推导困难时查阅。南瓜书的目标是帮助读者成为“理工科数学基础扎实点的大二下学生”。谢谢你的提问！\n"
     ]
    }
   ],
   "source": [
    "result = qa_chain.invoke(question_1)\n",
    "print(\"大模型+知识库后回答 question_1 的结果：\")\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "大模型+知识库后回答 question_2 的结果：\n",
      "《Prompt Engineering for Developer》课程是由吴恩达老师与 OpenAI 技术团队成员 Isa Fulford 合作授课的。谢谢你的提问！\n"
     ]
    }
   ],
   "source": [
    "result = qa_chain.invoke(question_2)\n",
    "print(\"大模型+知识库后回答 question_2 的结果：\")\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**大模型自己回答的效果**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'南瓜书通常是指《深度学习：算法与实现》这本书，因为书的封面是橙色的，看起来像南瓜。这本书由李沐、阿斯顿张、扎卡里·C·立顿和亚历山大·J·斯莫拉编写，主要介绍深度学习的基础知识和实践方法。书中涵盖了深度学习的基本概念、常用模型和算法，并提供了大量的代码示例，帮助读者理解和实现深度学习技术。由于其内容详实且实用性强，南瓜书在深度学习领域的学习者中受到广泛欢迎。'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "llm.invoke(question_1).content"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'《Prompt Engineering for Developers》是由 Isa Fulford 和 Andrew Ng 合著的一本书。该书旨在帮助开发者更好地理解和应用提示工程（Prompt Engineering）技术，以提高与大型语言模型（如GPT-3）交互的效率和效果。'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "llm.invoke(question_2).content"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> ⭐ 通过以上两个问题，我们发现 LLM 对于一些近几年的知识以及非常识性的专业问题，回答的并不是很好。而加上我们的本地知识，就可以帮助 LLM 做出更好的回答。另外，也有助于缓解大模型的“幻觉”问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.2.5 向检索链添加聊天记录"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们已经实现了通过上传本地知识文档，然后将他们保存到向量知识库，通过将查询问题与向量知识库的召回结果进行结合输入到 LLM 中，我们就得到了一个相比于直接让 LLM 回答要好得多的结果。在与语言模型交互时，你可能已经注意到一个关键问题 - **它们并不记得你之前的交流内容**。这在我们构建一些应用程序（如聊天机器人）的时候，带来了很大的挑战，使得对话似乎缺乏真正的连续性。这个问题该如何解决呢？\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**传递聊天记录**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在本节中我们将使用 LangChain 中的`ChatPromptTemplate`，即将先前的对话嵌入到语言模型中，使其具有连续对话的能力。 `ChatPromptTemplate`可以接收聊天消息历史记录，这些历史记录将在回答问题时与问题一起传递给聊天机器人，从而将它们添加到上下文中。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "\n",
    "# 问答链的系统prompt\n",
    "system_prompt = (\n",
    "    \"你是一个问答任务的助手。 \"\n",
    "    \"请使用检索到的上下文片段回答这个问题。 \"\n",
    "    \"如果你不知道答案就说不知道。 \"\n",
    "    \"请使用简洁的话语回答用户。\"\n",
    "    \"\\n\\n\"\n",
    "    \"{context}\"\n",
    ")\n",
    "# 制定prompt template\n",
    "qa_prompt = ChatPromptTemplate(\n",
    "    [\n",
    "        (\"system\", system_prompt),\n",
    "        (\"placeholder\", \"{chat_history}\"),\n",
    "        (\"human\", \"{input}\"),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "你是一个问答任务的助手。 请使用检索到的上下文片段回答这个问题。 如果你不知道答案就说不知道。 请使用简洁的话语回答用户。\n",
      "\n",
      "\n",
      "南瓜书是什么？\n"
     ]
    }
   ],
   "source": [
    "# 无历史记录\n",
    "messages = qa_prompt.invoke(\n",
    "    {\n",
    "        \"input\": \"南瓜书是什么？\",\n",
    "        \"chat_history\": [],\n",
    "        \"context\": \"\"\n",
    "    }\n",
    ")\n",
    "for message in messages.messages:\n",
    "    print(message.content)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "你是一个问答任务的助手。 请使用检索到的上下文片段回答这个问题。 如果你不知道答案就说不知道。 请使用简洁的话语回答用户。\n",
      "\n",
      "\n",
      "西瓜书是什么？\n",
      "西瓜书是指周志华老师的《机器学习》一书，是机器学习领域的经典入门教材之一。\n",
      "你可以介绍一下他吗？\n"
     ]
    }
   ],
   "source": [
    "# 有历史记录\n",
    "messages = qa_prompt.invoke(\n",
    "    {\n",
    "        \"input\": \"你可以介绍一下他吗？\",\n",
    "        \"chat_history\": [\n",
    "            (\"human\", \"西瓜书是什么？\"),\n",
    "            (\"ai\", \"西瓜书是指周志华老师的《机器学习》一书，是机器学习领域的经典入门教材之一。\"),\n",
    "        ],\n",
    "        \"context\": \"\"\n",
    "    }\n",
    ")\n",
    "for message in messages.messages:\n",
    "    print(message.content)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.2.6 带有信息压缩的检索链"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因为我们正在搭建的问答链带有支持多轮对话功能，所以与单轮对话的问答链相比会多面临像上方输出结果的问题，即用户最新的对话语义不全，在使用用户问题查询向量数据库时很难检索到相关信息。像上方的“你可以介绍一下他吗？”，其实是“你可以介绍下周志华老师吗？”的意思。为了解决这个问题我们将采取信息压缩的方式，让llm根据历史记录完善用户的问题。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.runnables import RunnableBranch\n",
    "\n",
    "# 压缩问题的系统 prompt\n",
    "condense_question_system_template = (\n",
    "    \"请根据聊天记录完善用户最新的问题，\"\n",
    "    \"如果用户最新的问题不需要完善则返回用户的问题。\"\n",
    "    )\n",
    "# 构造 压缩问题的 prompt template\n",
    "condense_question_prompt = ChatPromptTemplate([\n",
    "        (\"system\", condense_question_system_template),\n",
    "        (\"placeholder\", \"{chat_history}\"),\n",
    "        (\"human\", \"{input}\"),\n",
    "    ])\n",
    "# 构造检索文档的链\n",
    "# RunnableBranch 会根据条件选择要运行的分支\n",
    "retrieve_docs = RunnableBranch(\n",
    "    # 分支 1: 若聊天记录中没有 chat_history 则直接使用用户问题查询向量数据库\n",
    "    (lambda x: not x.get(\"chat_history\", False), (lambda x: x[\"input\"]) | retriever, ),\n",
    "    # 分支 2 : 若聊天记录中有 chat_history 则先让 llm 根据聊天记录完善问题再查询向量数据库\n",
    "    condense_question_prompt | llm | StrOutputParser() | retriever,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**支持聊天记录的检索问答链**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里我们使用之前定义的问答模板即`qa_prompt`构造问答链，另外我们通过`RunnablePassthrough.assign`将中间的查询结果存为`\"context\"`，将最终结果存为`\"answer\"`。因为查询结果被存为`\"context\"`，所以我们整合查询结果的函数`combine_docs`也要做相应的改动。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 重新定义 combine_docs\n",
    "def combine_docs(docs):\n",
    "    return \"\\n\\n\".join(doc.page_content for doc in docs[\"context\"]) # 将 docs 改为 docs[\"context\"]\n",
    "# 定义问答链\n",
    "qa_chain = (\n",
    "    RunnablePassthrough.assign(context=combine_docs) # 使用 combine_docs 函数整合 qa_prompt 中的 context\n",
    "    | qa_prompt # 问答模板\n",
    "    | llm\n",
    "    | StrOutputParser() # 规定输出的格式为 str\n",
    ")\n",
    "# 定义带有历史记录的问答链\n",
    "qa_history_chain = RunnablePassthrough.assign(\n",
    "    context = (lambda x: x) | retrieve_docs # 将查询结果存为 content\n",
    "    ).assign(answer=qa_chain) # 将最终结果存为 answer"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**测试检索问答链**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'input': '西瓜书是什么？',\n",
       " 'chat_history': [],\n",
       " 'context': [Document(metadata={'author': '', 'creationDate': \"D:20230303170709-00'00'\", 'creator': 'LaTeX with hyperref', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'format': 'PDF 1.5', 'keywords': '', 'modDate': '', 'page': 1, 'producer': 'xdvipdfmx (20200315)', 'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'subject': '', 'title': '', 'total_pages': 196, 'trapped': ''}, page_content='前言\\n“周志华老师的《机器学习》（西瓜书）是机器学习领域的经典入门教材之一，周老师为了使尽可能多的读\\n者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述，但是这对那些想深究公式推\\n导细节的读者来说可能“不太友好”，本书旨在对西瓜书里比较难理解的公式加以解析，以及对部分公式补充\\n具体的推导细节。”\\n读到这里，大家可能会疑问为啥前面这段话加了引号，因为这只是我们最初的遐想，后来我们了解到，周\\n老师之所以省去这些推导细节的真实原因是，他本尊认为“理工科数学基础扎实点的大二下学生应该对西瓜书\\n中的推导细节无困难吧，要点在书里都有了，略去的细节应能脑补或做练习”。所以...... 本南瓜书只能算是我\\n等数学渣渣在自学的时候记下来的笔记，希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二\\n下学生”。\\n使用说明\\n• 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的，所以南瓜书的最佳使用方法是以西瓜书\\n为主线，遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书；\\n• 对于初学机器学习的小白，西瓜书第1 章和第2 章的公式强烈不建议深究，简单过一下即可，等你学得'),\n",
       "  Document(metadata={'author': '', 'creationDate': \"D:20230303170709-00'00'\", 'creator': 'LaTeX with hyperref', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'format': 'PDF 1.5', 'keywords': '', 'modDate': '', 'page': 161, 'producer': 'xdvipdfmx (20200315)', 'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'subject': '', 'title': '', 'total_pages': 196, 'trapped': ''}, page_content='对于“误差”、“损失”、“风险”等概念的辨析，参见“西瓜书”第2 章2.1 节的注解。\\n→_→\\n配套视频教程：https://www.bilibili.com/video/BV1Mh411e7VU\\n←_←'),\n",
       "  Document(metadata={'author': '', 'creationDate': \"D:20230303170709-00'00'\", 'creator': 'LaTeX with hyperref', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'format': 'PDF 1.5', 'keywords': '', 'modDate': '', 'page': 1, 'producer': 'xdvipdfmx (20200315)', 'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'subject': '', 'title': '', 'total_pages': 196, 'trapped': ''}, page_content='• 对于初学机器学习的小白，西瓜书第1 章和第2 章的公式强烈不建议深究，简单过一下即可，等你学得\\n有点飘的时候再回来啃都来得及；\\n• 每个公式的解析和推导我们都力(zhi) 争(neng) 以本科数学基础的视角进行讲解，所以超纲的数学知识\\n我们通常都会以附录和参考文献的形式给出，感兴趣的同学可以继续沿着我们给的资料进行深入学习；\\n• 若南瓜书里没有你想要查阅的公式，或者你发现南瓜书哪个地方有错误，请毫不犹豫地去我们GitHub 的\\nIssues（地址：https://github.com/datawhalechina/pumpkin-book/issues）进行反馈，在对应版块\\n提交你希望补充的公式编号或者勘误信息，我们通常会在24 小时以内给您回复，超过24 小时未回复的\\n话可以微信联系我们（微信号：at-Sm1les）；\\n配套视频教程：https://www.bilibili.com/video/BV1Mh411e7VU\\n在线阅读地址：https://datawhalechina.github.io/pumpkin-book（仅供第1 版）')],\n",
       " 'answer': '西瓜书是指周志华老师的《机器学习》一书，它是机器学习领域的经典入门教材之一。'}"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 不带聊天记录\n",
    "qa_history_chain.invoke({\n",
    "    \"input\": \"西瓜书是什么？\",\n",
    "    \"chat_history\": []\n",
    "})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'input': '南瓜书跟它有什么关系？',\n",
       " 'chat_history': [('human', '西瓜书是什么？'),\n",
       "  ('ai', '西瓜书是指周志华老师的《机器学习》一书，是机器学习领域的经典入门教材之一。')],\n",
       " 'context': [Document(metadata={'author': '', 'creationDate': \"D:20230303170709-00'00'\", 'creator': 'LaTeX with hyperref', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'format': 'PDF 1.5', 'keywords': '', 'modDate': '', 'page': 1, 'producer': 'xdvipdfmx (20200315)', 'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'subject': '', 'title': '', 'total_pages': 196, 'trapped': ''}, page_content='前言\\n“周志华老师的《机器学习》（西瓜书）是机器学习领域的经典入门教材之一，周老师为了使尽可能多的读\\n者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述，但是这对那些想深究公式推\\n导细节的读者来说可能“不太友好”，本书旨在对西瓜书里比较难理解的公式加以解析，以及对部分公式补充\\n具体的推导细节。”\\n读到这里，大家可能会疑问为啥前面这段话加了引号，因为这只是我们最初的遐想，后来我们了解到，周\\n老师之所以省去这些推导细节的真实原因是，他本尊认为“理工科数学基础扎实点的大二下学生应该对西瓜书\\n中的推导细节无困难吧，要点在书里都有了，略去的细节应能脑补或做练习”。所以...... 本南瓜书只能算是我\\n等数学渣渣在自学的时候记下来的笔记，希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二\\n下学生”。\\n使用说明\\n• 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的，所以南瓜书的最佳使用方法是以西瓜书\\n为主线，遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书；\\n• 对于初学机器学习的小白，西瓜书第1 章和第2 章的公式强烈不建议深究，简单过一下即可，等你学得'),\n",
       "  Document(metadata={'author': '', 'creationDate': \"D:20230303170709-00'00'\", 'creator': 'LaTeX with hyperref', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'format': 'PDF 1.5', 'keywords': '', 'modDate': '', 'page': 13, 'producer': 'xdvipdfmx (20200315)', 'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'subject': '', 'title': '', 'total_pages': 196, 'trapped': ''}, page_content='→_→\\n欢迎去各大电商平台选购纸质版南瓜书《机器学习公式详解》\\n←_←\\n第1 章\\n绪论\\n本章作为“西瓜书”的开篇，主要讲解什么是机器学习以及机器学习的相关数学符号，为后续内容作\\n铺垫，并未涉及复杂的算法理论，因此阅读本章时只需耐心梳理清楚所有概念和数学符号即可。此外，在\\n阅读本章前建议先阅读西瓜书目录前页的《主要符号表》，它能解答在阅读“西瓜书”过程中产生的大部\\n分对数学符号的疑惑。\\n本章也作为本书的开篇，笔者在此赘述一下本书的撰写初衷，本书旨在以“过来人”的视角陪读者一\\n起阅读“西瓜书”，尽力帮读者消除阅读过程中的“数学恐惧”，只要读者学习过《高等数学》、《线性代\\n数》和《概率论与数理统计》这三门大学必修的数学课，均能看懂本书对西瓜书中的公式所做的解释和推\\n导，同时也能体会到这三门数学课在机器学习上碰撞产生的“数学之美”。\\n1.1\\n引言\\n本节以概念理解为主，在此对“算法”和“模型”作补充说明。“算法”是指从数据中学得“模型”的具\\n体方法，例如后续章节中将会讲述的线性回归、对数几率回归、决策树等。“算法”产出的结果称为“模型”，'),\n",
       "  Document(metadata={'author': '', 'creationDate': \"D:20230303170709-00'00'\", 'creator': 'LaTeX with hyperref', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'format': 'PDF 1.5', 'keywords': '', 'modDate': '', 'page': 1, 'producer': 'xdvipdfmx (20200315)', 'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'subject': '', 'title': '', 'total_pages': 196, 'trapped': ''}, page_content='• 对于初学机器学习的小白，西瓜书第1 章和第2 章的公式强烈不建议深究，简单过一下即可，等你学得\\n有点飘的时候再回来啃都来得及；\\n• 每个公式的解析和推导我们都力(zhi) 争(neng) 以本科数学基础的视角进行讲解，所以超纲的数学知识\\n我们通常都会以附录和参考文献的形式给出，感兴趣的同学可以继续沿着我们给的资料进行深入学习；\\n• 若南瓜书里没有你想要查阅的公式，或者你发现南瓜书哪个地方有错误，请毫不犹豫地去我们GitHub 的\\nIssues（地址：https://github.com/datawhalechina/pumpkin-book/issues）进行反馈，在对应版块\\n提交你希望补充的公式编号或者勘误信息，我们通常会在24 小时以内给您回复，超过24 小时未回复的\\n话可以微信联系我们（微信号：at-Sm1les）；\\n配套视频教程：https://www.bilibili.com/video/BV1Mh411e7VU\\n在线阅读地址：https://datawhalechina.github.io/pumpkin-book（仅供第1 版）')],\n",
       " 'answer': '南瓜书是对西瓜书中较难理解的公式进行解析和推导细节补充的书籍。它以西瓜书的内容为前置知识，帮助读者更好地理解和学习西瓜书中的内容。'}"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 带聊天记录\n",
    "qa_history_chain.invoke({\n",
    "    \"input\": \"南瓜书跟它有什么关系？\",\n",
    "    \"chat_history\": [\n",
    "        (\"human\", \"西瓜书是什么？\"),\n",
    "        (\"ai\", \"西瓜书是指周志华老师的《机器学习》一书，是机器学习领域的经典入门教材之一。\"),\n",
    "    ]\n",
    "})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，LLM 准确地判断了“它”是什么，代表着我们成功地传递给了它历史信息。另外召回的内容也有着问题的答案，证明我们的信息压缩策略也起到了作用。这种关联前后问题及压缩信息并检索的能力，可大大增强问答系统的连续性和智能水平。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4.3 部署知识库助手\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们对知识库和LLM已经有了基本的理解，现在是将它们巧妙地融合并打造成一个富有视觉效果的界面的时候了。这样的界面不仅对操作更加便捷，还能便于与他人分享。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Streamlit 是一种快速便捷的方法，可以直接在 **Python 中通过友好的 Web 界面演示机器学习模型**。在本课程中，我们将学习*如何使用它为生成式人工智能应用程序构建用户界面*。在构建了机器学习模型后，如果你想构建一个 demo 给其他人看，也许是为了获得反馈并推动系统的改进，或者只是因为你觉得这个系统很酷，所以想演示一下：Streamlit 可以让您通过 Python 接口程序快速实现这一目标，而无需编写任何前端、网页或 JavaScript 代码。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.3.1 Streamlit 简介"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "`Streamlit` 是一个用于快速创建数据应用程序的开源 Python 库。它的设计目标是让数据科学家能够轻松地将数据分析和机器学习模型转化为具有交互性的 Web 应用程序，而无需深入了解 Web 开发。和常规 Web 框架，如 Flask/Django 的不同之处在于，它不需要你去编写任何客户端代码（HTML/CSS/JS），只需要编写普通的 Python 模块，就可以在很短的时间内创建美观并具备高度交互性的界面，从而快速生成数据分析或者机器学习的结果；另一方面，和那些只能通过拖拽生成的工具也不同的是，你仍然具有对代码的完整控制权。\n",
    "\n",
    "Streamlit 提供了一组简单而强大的基础模块，用于构建数据应用程序：\n",
    "\n",
    "- st.write()：这是最基本的模块之一，用于在应用程序中呈现文本、图像、表格等内容。\n",
    "\n",
    "- st.title()、st.header()、st.subheader()：这些模块用于添加标题、子标题和分组标题，以组织应用程序的布局。\n",
    "\n",
    "- st.text()、st.markdown()：用于添加文本内容，支持 Markdown 语法。\n",
    "\n",
    "- st.image()：用于添加图像到应用程序中。\n",
    "\n",
    "- st.dataframe()：用于呈现 Pandas 数据框。\n",
    "\n",
    "- st.table()：用于呈现简单的数据表格。\n",
    "\n",
    "- st.pyplot()、st.altair_chart()、st.plotly_chart()：用于呈现 Matplotlib、Altair 或 Plotly 绘制的图表。\n",
    "\n",
    "- st.selectbox()、st.multiselect()、st.slider()、st.text_input()：用于添加交互式小部件，允许用户在应用程序中进行选择、输入或滑动操作。\n",
    "\n",
    "- st.button()、st.checkbox()、st.radio()：用于添加按钮、复选框和单选按钮，以触发特定的操作。\n",
    "\n",
    "这些基础模块使得通过 Streamlit 能够轻松地构建交互式数据应用程序，并且在使用时可以根据需要进行组合和定制，更多内容请查看[官方文档](https://docs.streamlit.io/get-started)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.3.2 构建应用程序"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先，创建一个新的 Python 文件并将其保存 streamlit_app.py在工作目录的根目录中"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. 导入必要的 Python 库。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import streamlit as st\n",
    "from langchain_openai import ChatOpenAI\n",
    "import os\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_core.runnables import RunnableBranch, RunnablePassthrough\n",
    "import sys\n",
    "sys.path.append(\"notebook/C3 搭建知识库\") # 将父目录放入系统路径中\n",
    "from zhipuai_embedding import ZhipuAIEmbeddings\n",
    "from langchain_community.vectorstores import Chroma"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. 定义`get_retriever`函数，该函数返回一个检索器"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_retriever():\n",
    "    # 定义 Embeddings\n",
    "    embedding = ZhipuAIEmbeddings()\n",
    "    # 向量数据库持久化路径\n",
    "    persist_directory = 'data_base/vector_db/chroma'\n",
    "    # 加载数据库\n",
    "    vectordb = Chroma(\n",
    "        persist_directory=persist_directory,\n",
    "        embedding_function=embedding\n",
    "    )\n",
    "    return vectordb.as_retriever()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. 定义`combine_docs`函数， 该函数处理检索器返回的文本"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def combine_docs(docs):\n",
    "    return \"\\n\\n\".join(doc.page_content for doc in docs[\"context\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "4. 定义`get_qa_history_chain`函数，该函数可以返回一个检索问答链"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_qa_history_chain():\n",
    "    retriever = get_retriever()\n",
    "    llm = ChatOpenAI(model_name=\"gpt-4o\", temperature=0)\n",
    "    condense_question_system_template = (\n",
    "        \"请根据聊天记录总结用户最近的问题，\"\n",
    "        \"如果没有多余的聊天记录则返回用户的问题。\"\n",
    "    )\n",
    "    condense_question_prompt = ChatPromptTemplate([\n",
    "            (\"system\", condense_question_system_template),\n",
    "            (\"placeholder\", \"{chat_history}\"),\n",
    "            (\"human\", \"{input}\"),\n",
    "        ])\n",
    "\n",
    "    retrieve_docs = RunnableBranch(\n",
    "        (lambda x: not x.get(\"chat_history\", False), (lambda x: x[\"input\"]) | retriever, ),\n",
    "        condense_question_prompt | llm | StrOutputParser() | retriever,\n",
    "    )\n",
    "\n",
    "    system_prompt = (\n",
    "        \"你是一个问答任务的助手。 \"\n",
    "        \"请使用检索到的上下文片段回答这个问题。 \"\n",
    "        \"如果你不知道答案就说不知道。 \"\n",
    "        \"请使用简洁的话语回答用户。\"\n",
    "        \"\\n\\n\"\n",
    "        \"{context}\"\n",
    "    )\n",
    "    qa_prompt = ChatPromptTemplate.from_messages(\n",
    "        [\n",
    "            (\"system\", system_prompt),\n",
    "            (\"placeholder\", \"{chat_history}\"),\n",
    "            (\"human\", \"{input}\"),\n",
    "        ]\n",
    "    )\n",
    "    qa_chain = (\n",
    "        RunnablePassthrough().assign(context=combine_docs)\n",
    "        | qa_prompt\n",
    "        | llm\n",
    "        | StrOutputParser()\n",
    "    )\n",
    "\n",
    "    qa_history_chain = RunnablePassthrough().assign(\n",
    "        context = retrieve_docs, \n",
    "        ).assign(answer=qa_chain)\n",
    "    return qa_history_chain"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "5. 定义`gen_response`函数，它接受检索问答链、用户输入及聊天历史，并以流式返回该链输出"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def gen_response(chain, input, chat_history):\n",
    "    response = chain.stream({\n",
    "        \"input\": input,\n",
    "        \"chat_history\": chat_history\n",
    "    })\n",
    "    for res in response:\n",
    "        if \"answer\" in res.keys():\n",
    "            yield res[\"answer\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "6. 定义main函数，该函数制定显示效果与逻辑"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def main():\n",
    "    st.markdown('### 🦜🔗 动手学大模型应用开发')\n",
    "    # st.session_state可以存储用户与应用交互期间的状态与数据\n",
    "    # 存储对话历史\n",
    "    if \"messages\" not in st.session_state:\n",
    "        st.session_state.messages = []\n",
    "    # 存储检索问答链\n",
    "    if \"qa_history_chain\" not in st.session_state:\n",
    "        st.session_state.qa_history_chain = get_qa_history_chain()\n",
    "    # 建立容器 高度为500 px\n",
    "    messages = st.container(height=550)\n",
    "    # 显示整个对话历史\n",
    "    for message in st.session_state.messages: # 遍历对话历史\n",
    "            with messages.chat_message(message[0]): # messages指在容器下显示，chat_message显示用户及ai头像\n",
    "                st.write(message[1]) # 打印内容\n",
    "    if prompt := st.chat_input(\"Say something\"):\n",
    "        # 将用户输入添加到对话历史中\n",
    "        st.session_state.messages.append((\"human\", prompt))\n",
    "        # 显示当前用户输入\n",
    "        with messages.chat_message(\"human\"):\n",
    "            st.write(prompt)\n",
    "        # 生成回复\n",
    "        answer = gen_response(\n",
    "            chain=st.session_state.qa_history_chain,\n",
    "            input=prompt,\n",
    "            chat_history=st.session_state.messages\n",
    "        )\n",
    "        # 流式输出\n",
    "        with messages.chat_message(\"ai\"):\n",
    "            output = st.write_stream(answer)\n",
    "        # 将输出存入st.session_state.messages\n",
    "        st.session_state.messages.append((\"ai\", output))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.3.3 部署应用程序  \n",
    "\n",
    "**本地运行：** `streamlit run \"notebook/C4 构建 RAG 应用/streamlit_app.py\"`\n",
    "**远程部署：**要将应用程序部署到 Streamlit Cloud，请执行以下步骤：  \n",
    "  \n",
    "1. 为应用程序创建 GitHub 存储库。您的存储库应包含两个文件：  \n",
    "  \n",
    "   your-repository/  \n",
    "   ├── streamlit_app.py  \n",
    "   └── requirements.txt  \n",
    "  \n",
    "2. 转到 [Streamlit Community Cloud](http://share.streamlit.io/)，单击工作区中的`New app`按钮，然后指定存储库、分支和主文件路径。或者，您可以通过选择自定义子域来自定义应用程序的 URL\n",
    "  \n",
    "3. 点击`Deploy!`按钮  \n",
    "  \n",
    "您的应用程序现在将部署到 Streamlit Community Cloud，并且可以从世界各地访问！ 🌎  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们的项目部署到这基本完成，为了方便进行演示进行了简化，还有很多可以进一步优化的地方，期待学习者们进行各种魔改！\n",
    "\n",
    "优化方向：\n",
    "- 界面中添加上传本地文档，建立向量数据库的功能\n",
    "- 添加多种LLM 与 embedding方法选择的按钮\n",
    "- 添加修改参数的按钮\n",
    "- 更多......"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "universe_0_3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
